feat: add Edge/Chromium browser extension support#138
feat: add Edge/Chromium browser extension support#138phelix001 wants to merge 5 commits intolinux-credentials:mainfrom
Conversation
Port the Firefox web extension to Edge/Chromium (MV3, Chrome 111+). Key architectural differences from Firefox version: - Two content scripts: MAIN world (overrides navigator.credentials) and ISOLATED world (bridges to background via chrome.runtime) - window.postMessage bridge between MAIN and ISOLATED worlds (Firefox uses exportFunction/cloneInto which don't exist in Chromium) - Base64url encoding via btoa/atob helpers instead of Uint8Array.toBase64/fromBase64 (not available in Chromium) - Service worker background script instead of persistent background page - chrome.* namespace instead of browser.* New files: - webext/add-on-edge/ - Complete Edge/Chromium extension - webext/app/credential_manager_shim_edge.json.in - Native messaging manifest template for Chromium-based browsers Updated README with Edge/Chromium setup instructions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
iinuwa
left a comment
There was a problem hiding this comment.
Hello! Thanks for looking into this!
I haven't gotten the chance to test this out yet, but I did an initial look through, and there's quite a bit of duplicated code. We hope not to have to keep this around long term, but I still think it'd be helpful not to duplicate the code.
I think this means that I'd like to see if we can keep all the JavaScript files in the one add-on folder, with different manifests and "utils" files that contain the differences between Firefox and Chromium, and a check at runtime to import the correct one. If that means creating the extra "bridge" port in Firefox and/or a shim of cloneInto() for Chromium even if it's technically unnecessary, then that's fine with me.
Then we'd use Meson to bundle the add-ons for each browser platform.
I can help with the Meson parts; would you be willing to look into merging these two folders together?
webext/add-on-edge/background.js
Outdated
| function arrayBufferToBase64url(buffer) { | ||
| const bytes = new Uint8Array(buffer); | ||
| let binary = ''; | ||
| for (let i = 0; i < bytes.length; i++) { | ||
| binary += String.fromCharCode(bytes[i]); | ||
| } | ||
| return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); | ||
| } | ||
|
|
||
| function base64urlToBytes(str) { | ||
| if (!str) return null; | ||
| const padded = str.replace(/-/g, '+').replace(/_/g, '/'); | ||
| const binary = atob(padded); | ||
| const bytes = new Uint8Array(binary.length); | ||
| for (let i = 0; i < binary.length; i++) { | ||
| bytes[i] = binary.charCodeAt(i); | ||
| } | ||
| return bytes; | ||
| } |
There was a problem hiding this comment.
This is built into Chromium as Uint8Array.from/toBase64; can we use those?
There was a problem hiding this comment.
Done — switched to native Uint8Array.toBase64() / fromBase64() with {alphabet: "base64url", omitPadding: true} throughout. The manual btoa/atob helpers are removed entirely.
webext/add-on-edge/content-main.js
Outdated
| // Base64url helpers (Chromium doesn't have Uint8Array.toBase64/fromBase64) | ||
| function arrayBufferToBase64url(buffer) { | ||
| const bytes = new Uint8Array(buffer); | ||
| let binary = ''; | ||
| for (let i = 0; i < bytes.length; i++) { | ||
| binary += String.fromCharCode(bytes[i]); | ||
| } | ||
| return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); | ||
| } | ||
|
|
||
| function base64urlToArrayBuffer(str) { | ||
| if (!str) return null; | ||
| const padded = str.replace(/-/g, '+').replace(/_/g, '/'); | ||
| const binary = atob(padded); | ||
| const bytes = new Uint8Array(binary.length); | ||
| for (let i = 0; i < binary.length; i++) { | ||
| bytes[i] = binary.charCodeAt(i); | ||
| } | ||
| return bytes.buffer; | ||
| } |
There was a problem hiding this comment.
Same here; the comment is out of date: Chrome has had these for about 6 months.
There was a problem hiding this comment.
Fixed — removed the outdated comment and the manual helpers. Using native Uint8Array.toBase64() / fromBase64() here as well.
Address PR review feedback to eliminate code duplication between webext/add-on/ (Firefox) and webext/add-on-edge/ (Chromium). Key changes: - Unified architecture: both browsers now use MAIN + ISOLATED world content scripts with window.postMessage bridge, eliminating the need for Firefox-specific cloneInto()/exportFunction() APIs - Use native Uint8Array.toBase64()/fromBase64() for base64url encoding/decoding (supported in both Firefox 140+ and Chrome 111+) - Simplified background.js: ArrayBuffer serialization now happens in content-main.js, so background just forwards messages - Browser-specific manifests: manifest.firefox.json (background scripts) and manifest.chromium.json (service worker) - Browser API detection via globalThis.browser || globalThis.chrome in content-bridge.js and background.js Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Thanks for the review! I've pushed a commit that addresses all the feedback: Merged into a single
Browser-specific manifests: Native Browser API detection via Firefox now uses the same MAIN + ISOLATED world architecture as Chromium (with the bridge port as you suggested), which eliminates the need for I left a TODO in meson.build for the Chromium build target — happy to take your help on that part. |
|
Pushed two new commits on top of the refactor: docs: update READMEs for unified browser extension (
ci: pin Rust toolchain, update actions, add audit and JS checks (
Happy to split the CI/toolchain changes into a separate PR if you'd prefer to keep this one focused on the web extension work. |
- Update README to document Edge/Chromium support alongside Firefox - Fix webext/README references to deleted add-on-edge/ directory - Add manifest.json copy step for both Firefox and Chromium dev workflows - Add webext/add-on/manifest.json to .gitignore (generated for local dev) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1f2f730 to
612f201
Compare
|
Thanks for the update! I'm hoping to get to this sometime this weekend. In the meantime can you remove the CLAUDE.md? We're not ready to declare a policy on AI usage yet. The unrelated CI changes should also move to a separate PR to make this one easier to review. |
612f201 to
58d581e
Compare
|
Done! Two changes:
This PR is now 3 commits, all focused on the web extension work. @iinuwa ready when you are this weekend. |
iinuwa
left a comment
There was a problem hiding this comment.
Sorry for the long delay in review. I tested and this works for both Chrome and Firefox, thanks! (There a few bugs unrelated to this feature that can make startup inconsistent.)
I have a few more changes to make.
webext/add-on/manifest.firefox.json
Outdated
| "matches": ["https://webauthn.io/*", "https://demo.yubico.com/*"], | ||
| "js": ["content.js"], | ||
| "run_at": "document_start" | ||
| "matches": ["<all_urls>"], |
There was a problem hiding this comment.
A change like this will prompt extra review in the Firefox add-ons store, which I'm not ready to do at this time. Can you change this back?
There was a problem hiding this comment.
Done — reverted both content_scripts entries back to the restricted URL list.
webext/add-on/manifest.firefox.json
Outdated
| "world": "ISOLATED" | ||
| }, | ||
| { | ||
| "matches": ["<all_urls>"], |
| }, | ||
|
|
||
| "background": { | ||
| "service_worker": "background.js" |
There was a problem hiding this comment.
The only difference between the Firefox manifest and the Chromium manifest is that one uses background.scripts and this uses background.service_worker? (The browser_specific_settings key can be combined as Chrome ignores it silently.)
Can we use the same method in both browsers?
There was a problem hiding this comment.
Unfortunately not — Chrome MV3 requires service_worker and doesn't support background.scripts, so the only unified option would be switching Firefox to service_worker too. Since that would change the Firefox manifest and trigger extra store review, we need to keep the two separate manifests for now.
The only real difference between them is background.scripts vs background.service_worker (plus browser_specific_settings.gecko), so the duplication is minimal.
There was a problem hiding this comment.
I expect adding a service worker will trigger automated review (as opposed to a manual review for the <all_urls> change). Let's go ahead and use the service_worker method for both instead if that works.
There was a problem hiding this comment.
Done — switched Firefox manifest to service_worker as well. Both manifests now use the same background method.
webext/add-on/meson.build
Outdated
| # Shared JavaScript files used by both Firefox and Chromium builds | ||
| shared_js = ['background.js', 'content-bridge.js', 'content-main.js'] |
There was a problem hiding this comment.
| # Shared JavaScript files used by both Firefox and Chromium builds | |
| shared_js = ['background.js', 'content-bridge.js', 'content-main.js'] | |
| # Shared files used by both Firefox and Chromium builds | |
| shared_addon_files = ['icons' / 'logo.svg', 'background.js', 'content-bridge.js', 'content-main.js'] |
There was a problem hiding this comment.
Applied all the meson.build suggestions — shared file list renamed, Firefox target updated to use the cp/mv pattern, and Chromium target added. Thanks for the detailed suggestions.
webext/add-on/meson.build
Outdated
| shared_js = ['background.js', 'content-bridge.js', 'content-main.js'] | ||
|
|
||
| # Firefox XPI | ||
| firefox_files = ['manifest.firefox.json'] + shared_js + ['icons' / 'logo.svg'] |
There was a problem hiding this comment.
| firefox_files = ['manifest.firefox.json'] + shared_js + ['icons' / 'logo.svg'] | |
| firefox_addon_files = ['manifest.firefox.json'] + shared_addon_files |
webext/add-on/meson.build
Outdated
| 'xpi', | ||
| output: 'credentialsd-firefox-helper.xpi', | ||
| input: xpi_files, | ||
| input: firefox_files, |
There was a problem hiding this comment.
| input: firefox_files, | |
| input: firefox_addon_files, |
webext/add-on/meson.build
Outdated
| ) No newline at end of file | ||
| ) | ||
|
|
||
| # TODO: Add Chromium build target using manifest.chromium.json No newline at end of file |
There was a problem hiding this comment.
This creates a zip with the same structure as a CRX archive. You can't load it directly because it's not signed and is missing the CRX header, but if you unzip it, you can load the manifest.json directly.
The same commands should be used above for the Firefox target, except with firefox_addon_files, but Github won't let me write a suggestion for that
| # TODO: Add Chromium build target using manifest.chromium.json | |
| # Chromium extension (unsigned) | |
| # TODO: Wrap this in a self-signed CRX in CI | |
| chromium_addon_files = ['manifest.chromium.json'] + shared_addon_files | |
| custom_target( | |
| 'chromium-extension', | |
| output: 'credentialsd-chromium-helper.zip', | |
| input: chromium_addon_files, | |
| command: [ | |
| 'cp', '-r', '@INPUT@', '@PRIVATE_DIR@', | |
| '&&', | |
| 'cd', | |
| '@PRIVATE_DIR@', | |
| '&&', | |
| 'mv', '@PLAINNAME0@', 'manifest.json', | |
| '&&', | |
| 'mkdir', '-p', 'icons', | |
| '&&', | |
| 'mv', '@PLAINNAME1@', 'icons/', | |
| '&&', | |
| zip, | |
| '-r', | |
| '-FS', meson.project_build_root() / '@OUTPUT@', | |
| '.', | |
| ], | |
| install: true, | |
| install_dir: addon_dir, | |
| ) |
- Revert Firefox content_scripts matches from <all_urls> back to restricted URL list to avoid triggering extra Firefox store review - Apply reviewer's meson.build suggestions: rename shared file list, update Firefox target to use cp/mv pattern, add full Chromium build target Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch Firefox from background.scripts to background.service_worker to match the Chromium manifest, per reviewer feedback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Adds Edge/Chromium (Chrome 111+, Edge 111+) support to the browser extension, unified with the existing Firefox extension into a single codebase.
webext/add-on/with shared JS and browser-specific manifestsArchitecture
Both Firefox and Chromium now share the same content script architecture:
content-main.jsnavigator.credentials.create/get, serializes ArrayBuffers via nativeUint8Array.toBase64()/fromBase64()content-bridge.jswindow.postMessagetoruntime.connect()for native messagingbackground.jsBrowser differences handled via:
manifest.firefox.json— background scripts,gecko.strict_min_version: 140.0manifest.chromium.json— service worker, Chrome 111+globalThis.browser || globalThis.chromefor API detectionThe Python native messaging host (
credential_manager_shim.py) is reused unchanged.Commits
add-on-edge/directorycloneInto()/exportFunction()in favor of shared MAIN+ISOLATED architectureadd-on-edge/references in webext/READMETest plan
edge://extensions→ "Load unpacked" (copymanifest.chromium.jsontomanifest.jsonfirst)chrome://extensions) with equivalent setupmanifest.firefox.jsontomanifest.json)